iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Software Development

從 Fragment 到 Compose — 老 Android App 重寫的架構取捨系列 第 26 篇

Day 26|地圖與叢集:兩個「就緒」的競態

  • 分享至 

  • xImage
  •  

動筆前先答(兩題)

1.【預測再驗證|穩定性與記憶體】 地圖畫面要等兩件事:任務資料到了、地圖本身載好了。用 maps-compose 最直覺的骨架大概是這樣:

@Composable
fun TaskMapScreen(viewModel: TaskMapViewModel = hiltViewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    val cameraPositionState = rememberCameraPositionState()
    GoogleMap(
        cameraPositionState = cameraPositionState,
        onMapLoaded = { viewModel.onMapLoaded() },
    ) {
        state.tasks.forEach { task ->
            Marker(state = rememberMarkerState(position = task.position), title = task.name)
        }
    }
}

@HiltViewModel
class TaskMapViewModel @Inject constructor(repository: TaskRepository) : ViewModel() {
    private val mapLoaded = MutableStateFlow(false)

    val uiState: StateFlow<TaskMapUiState> = combine(repository.tasks, mapLoaded) { tasks, loaded ->
        TaskMapUiState(tasks = if (loaded) tasks else emptyList(), isLoading = !loaded)
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), TaskMapUiState())

    fun onMapLoaded() { mapLoaded.value = true }
}

下面每個情境,先寫下你的預測與依據,再驗證(maps-compose 的官方範例 App,或本專案的 demo flavor 加一個暫時的地圖畫面;旋轉、「開發者選項 → 不保留活動」、adb shell am kill <package> 都可以用):

  1. 地圖載好之後旋轉螢幕。onMapLoaded 會再被呼叫嗎?旋轉後那一刻 mapLoaded 的值是什麼?從旋轉到新地圖畫好之間的那段時間,使用者看到什麼?「這一個地圖實例載好了」跟「這個畫面的地圖載好過」是同一件事嗎?
  2. 地圖還沒載好,使用者就按返回離開。onMapLoaded 之後還會不會進來?會的話,mapLoaded.value = true 寫進了誰的 ViewModel、那個 ViewModel 什麼時候被清掉?下次再進地圖畫面,第一幀的 isLoading 是什麼?
  3. 地圖載好之前,任務資料連續變了三次(列表畫面剛同步完)。combine 的 lambda 總共跑幾次?地圖載好那一刻 marker 用的是哪一版資料?把 repository.tasks 從 StateFlow 換成普通 Flow,答案會變嗎?
  4. 有人把舊寫法搬過來:為了方便,在 ViewModel 裡放一個 GoogleMap(或 ClusterManager)的欄位,在 MapEffect 裡指派。旋轉三次、離開畫面、等幾秒。LeakCanary 會不會叫?會的話,報告裡 leak 路徑的第一個節點是誰?不會的話,為什麼 ViewModel 握著它沒事?
  5. 叢集:ClusterManager 綁定一個 GoogleMap 實例。旋轉後,舊的 manager 指向誰?用 MapEffect(key1 = …) 重建 manager 時,key 該放什麼?放錯會看到哪一種徵狀:marker 重複、marker 消失,還是點了沒反應?

最後回答:這五個情境裡,哪幾個可以在 JVM 單元測試裡用 Turbine 對著 ViewModel 驗?哪幾個要 instrumented test?哪幾個只能實機驗、要寫進發版前的手動檢查清單?

我的回答:(待補)

2.【決策邊界|規格與驗收】 舊 App 的地圖畫面是一個約 490 行的 Fragment,加上兩個畫面共用的 marker 管理(約 220 行)與叢集 renderer(約 105 行)。「就緒」的做法:ViewModel 放一個「可以更新地圖了」的旗標,地圖 callback 進來時設成 true(先檢查 view 還在、fragment 還 attached),onDestroyView 時重設;旗標為 true 之後才開始觀察定位與任務。另一個排序畫面把同一套握手再寫了一次。畫面另外觀察 10 個一次性的操作成功訊息。

新畫面要先決定什麼叫「就緒」。三個選項:

  • (A) 合成一個 UiState:Loading/Empty/Ready(tasks, bounds)/Error,由 combine(tasks, mapLoaded) 推導;地圖沒載好之前整個畫面蓋一層 loading。
  • (B) 不合成:地圖自己載、marker 自己來,兩者互不等待;載入中使用者看到的是空地圖。
  • (C) 沿用兩旗標握手:地圖就緒旗標在 ViewModel,資料流在就緒之後才開始收集。

不看下面的大綱,依序回答:

  1. 什麼條件下你會從 (A) 改選 (B)?寫出一個具體情境:網路狀況、任務數、配送員當時在做什麼。
  2. 空列表:在 (A) 裡 Empty 是終態嗎?地圖上要顯示什麼?如果「就緒」的定義裡偷偷含了「任務不是空的」,沒有任務的那一天使用者會看到什麼?把這條寫成驗收條件,而且要讓測試抓得到這個邊界。
  3. 地圖開著的時候任務變了(後台取消一單、推播進來)。相機要不要重新框住所有 marker?什麼條件下「相機跟著資料走」是對的、什麼條件下會惹惱剛放大看巷口的配送員?寫成一條規則。
  4. 舊畫面觀察的 10 個一次性成功訊息,在新設計裡屬於地圖畫面,還是屬於 Day 14 拆出來的任務操作 ViewModel?什麼條件下你會允許地圖 feature 自己複製一份?
  5. Day 26–27 是可裁切項。如果只能交「有 marker、沒有叢集」的版本,你的最低驗收是什麼?一天的任務數到多少,叢集從可選變成必要?這個數字你打算從哪裡拿:實際資料,還是先定一個再量?

最後用兩句話收尾:你的結論,以及你願意承認的代價。

我的回答:(待補)


上一篇
Day 25|掃描結果怎麼回到呼叫端:SavedStateHandle 與共享 ViewModel 的取捨
下一篇
Day 27|前景定位服務與 Compose 的溝通
系列文
從 Fragment 到 Compose — 老 Android App 重寫的架構取捨 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言